/*
 * Copyright 2010-2018 Eric Kok et al.
 *
 * Transdroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Transdroid is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.transdroid.core.gui;

import android.annotation.TargetApi;
import android.app.SearchManager;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout;

import com.getbase.floatingactionbutton.FloatingActionButton;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager;
import com.nispok.snackbar.enums.SnackbarType;

import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Background;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.FragmentById;
import org.androidannotations.annotations.InstanceState;
import org.androidannotations.annotations.OnActivityResult;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.SystemService;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.annotations.ViewById;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.transdroid.R;
import org.transdroid.core.app.search.SearchHelper_;
import org.transdroid.core.app.settings.ApplicationSettings;
import org.transdroid.core.app.settings.ServerSetting;
import org.transdroid.core.app.settings.SettingsUtils;
import org.transdroid.core.app.settings.SystemSettings;
import org.transdroid.core.app.settings.WebsearchSetting;
import org.transdroid.core.gui.lists.LocalTorrent;
import org.transdroid.core.gui.lists.SimpleListItem;
import org.transdroid.core.gui.log.Log;
import org.transdroid.core.gui.log.LogUncaughtExceptionHandler;
import org.transdroid.core.gui.navigation.FilterListAdapter;
import org.transdroid.core.gui.navigation.FilterListAdapter_;
import org.transdroid.core.gui.navigation.Label;
import org.transdroid.core.gui.navigation.NavigationFilter;
import org.transdroid.core.gui.navigation.NavigationHelper;
import org.transdroid.core.gui.navigation.RefreshableActivity;
import org.transdroid.core.gui.navigation.StatusType;
import org.transdroid.core.gui.rss.RssFeedsActivity_;
import org.transdroid.core.gui.search.FilePickerHelper;
import org.transdroid.core.gui.search.UrlEntryDialog;
import org.transdroid.core.gui.settings.MainSettingsActivity_;
import org.transdroid.core.service.AppUpdateJob;
import org.transdroid.core.service.ConnectivityHelper;
import org.transdroid.core.service.RssCheckerJob;
import org.transdroid.core.service.ServerCheckerJob;
import org.transdroid.core.widget.ListWidgetProvider;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentsSortBy;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.ForceRecheckTask;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetStatsTask;
import org.transdroid.daemon.task.GetStatsTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.PauseTask;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.ResumeTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetAlternativeModeTask;
import org.transdroid.daemon.task.SetDownloadLocationTask;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTrackersTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.task.StartTask;
import org.transdroid.daemon.task.StopTask;
import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask;
import org.transdroid.daemon.task.ToggleSequentialDownloadTask;
import org.transdroid.daemon.util.HttpHelper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Main activity that holds the fragment that shows the torrents list, presents a way to filter the list (via an action bar spinner or list side list)
 * and potentially shows a torrent details fragment too, if there is room. Task execution such as loading of and adding torrents is performs in this
 * activity, using background methods. Finally, the activity offers navigation elements such as access to settings and showing connection issues.
 *
 * @author Eric Kok
 */
@EActivity(R.layout.activity_torrents)
public class TorrentsActivity extends AppCompatActivity implements TorrentTasksExecutor, RefreshableActivity {

    private static final int RESULT_DETAILS = 0;

    // Fragment uses this to pause the refresh across restarts
    public boolean stopRefresh = false;

    // Navigation components
    @SystemService
    protected SearchManager searchManager;
    @Bean
    protected Log log;
    @Bean
    protected NavigationHelper navigationHelper;
    @Bean
    protected ConnectivityHelper connectivityHelper;
    @ViewById
    protected Toolbar selectionToolbar;
    @ViewById
    protected Toolbar torrentsToolbar;
    @ViewById
    protected Toolbar actionsToolbar;
    @ViewById(R.id.contextual_menu)
    protected ActionMenuView contextualMenu;
    @ViewById
    protected FloatingActionsMenu addmenuButton;
    @ViewById
    protected FloatingActionButton addmenuFileButton;
    @ViewById
    protected DrawerLayout drawerLayout;
    @ViewById(R.id.drawer_container)
    protected ViewGroup drawerContainer;
    @ViewById
    protected ListView drawerList;
    @ViewById
    protected ListView filtersList;
    @ViewById
    protected SearchView filterSearch;
    // Settings
    @Bean
    protected ApplicationSettings applicationSettings;
    @Bean
    protected SystemSettings systemSettings;
    @InstanceState
    protected NavigationFilter currentFilter = null;
    @InstanceState
    protected String preselectNavigationFilter = null;
    @InstanceState
    protected boolean turtleModeEnabled = false;
    @InstanceState
    protected ArrayList<Label> lastNavigationLabels;
    // Contained torrent and details fragments
    @FragmentById(R.id.torrents_fragment)
    protected TorrentsFragment fragmentTorrents;
    @FragmentById(R.id.torrentdetails_fragment)
    protected DetailsFragment fragmentDetails;
    @InstanceState
    boolean firstStart = true;
    private ListView navigationList;
    private FilterListAdapter navigationListAdapter;
    private ServerSelectionView serverSelectionView;
    private ServerStatusView serverStatusView;
    private ActionBarDrawerToggle drawerToggle;
    private MenuItem searchMenu = null;
    private IDaemonAdapter currentConnection = null;

    // Auto refresh task
    private AsyncTask<Void, Void, Void> autoRefreshTask;

    private String awaitingAddLocalFile;
    private String awaitingAddTitle;
    /**
     * Handles item selections on the dedicated list of filter items
     */
    private OnItemClickListener onFilterListItemClicked = new OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            navigationList.setItemChecked(position, true);
            Object item = navigationList.getAdapter().getItem(position);
            if (item instanceof SimpleListItem) {
                filterSelected((SimpleListItem) item, false);
            }
            if (drawerLayout != null)
                drawerLayout.closeDrawer(drawerContainer);
        }
    };
    private SearchView.OnQueryTextListener filterQueryTextChanged = new SearchView.OnQueryTextListener() {
        @Override
        public boolean onQueryTextSubmit(String query) {
            return false;
        }

        @Override
        public boolean onQueryTextChange(String newText) {
            // Redirect to filter method which will directly apply it
            filterTorrents(newText);
            return true;
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        SettingsUtils.applyDayNightTheme(this);

        // Catch any uncaught exception to log it
        Thread.setDefaultUncaughtExceptionHandler(new LogUncaughtExceptionHandler(this, Thread.getDefaultUncaughtExceptionHandler()));
        super.onCreate(savedInstanceState);
    }

    @AfterViews
    protected void init() {

        // Use custom views as action bar content, showing filter selection and current torrent counts/speeds
        serverSelectionView = ServerSelectionView_.build(this);
        serverStatusView = ServerStatusView_.build(this);
        if (selectionToolbar != null) {
            selectionToolbar.addView(serverSelectionView);
        } else {
            torrentsToolbar.addView(serverSelectionView);
        }
        actionsToolbar.addView(serverStatusView);
        // Redirect to the classic activity implementation so we can use @OptionsItem methods
        actionsToolbar.setOnMenuItemClickListener(this::onOptionsItemSelected);
        setSupportActionBar(torrentsToolbar); // For direct menu item inflation by the contained fragments
        getSupportActionBar().setDisplayShowTitleEnabled(false);

        // Construct the filters list, i.e. the list of servers, status types and labels
        navigationListAdapter = FilterListAdapter_.getInstance_(this);
        navigationListAdapter.updateServers(applicationSettings.getAllServerSettings());
        navigationListAdapter.updateStatusTypes(StatusType.getAllStatusTypes(this));
        // Add an empty labels list (which will be updated later, but the adapter needs to be created now)
        navigationListAdapter.updateLabels(new ArrayList<>());

        // Apply the filters list to the navigation drawer (on phones) or the dedicated side bar (i.e. on tablets)
        if (filtersList != null) {
            navigationList = filtersList;
        } else {
            navigationList = drawerList;
            drawerToggle =
                    new ActionBarDrawerToggle(this, drawerLayout, torrentsToolbar, R.string.navigation_opendrawer, R.string.navigation_closedrawer);
            drawerToggle.setDrawerIndicatorEnabled(true);
            drawerLayout.addDrawerListener(drawerToggle);
        }
        navigationList.setAdapter(navigationListAdapter);
        navigationList.setOnItemClickListener(onFilterListItemClicked);
        // Now that all items (or at least their adapters) have been added, ensure a filter is selected
        // NOTE When this is a fresh start, we might override the filter later (based on the last user selection)
        if (currentFilter == null) {
            currentFilter = StatusType.getShowAllType(this);
        }
        filterSearch.setOnQueryTextListener(filterQueryTextChanged);

        // Load the default server or a server that was explicitly supplied in the starting intent
        ServerSetting defaultServer = applicationSettings.getDefaultServer();
        if (defaultServer == null) {
            // No server settings yet
            return;
        }
        Torrent openTorrent = null;
        if (getIntent().getAction() != null && getIntent().getAction().equals(ListWidgetProvider.INTENT_STARTSERVER) &&
                getIntent().getExtras() == null && getIntent().hasExtra(ListWidgetProvider.EXTRA_SERVER)) {
            // A server settings order ID was provided in this org.transdroid.START_SERVER action intent
            int serverId = getIntent().getExtras().getInt(ListWidgetProvider.EXTRA_SERVER);
            if (serverId < 0 || serverId > applicationSettings.getMaxOfAllServers()) {
                log.e(this, "Tried to start with " + ListWidgetProvider.EXTRA_SERVER + " intent but " + serverId +
                        " is not an existing server order id");
            } else {
                defaultServer = applicationSettings.getServerSetting(serverId);
                if (getIntent().hasExtra(ListWidgetProvider.EXTRA_TORRENT)) {
                    openTorrent = getIntent().getParcelableExtra(ListWidgetProvider.EXTRA_TORRENT);
                }
            }
        }

        // Connect to the last used server or a server that was explicitly supplied in the starting intent
        if (firstStart) {
            // Force first torrents refresh
            filterSelected(defaultServer, true);
            // Perhaps we can select the last used navigation filter, but only after a first refresh was completed
            preselectNavigationFilter = applicationSettings.getLastUsedNavigationFilter();
            // Handle any start up intents
            if (openTorrent != null) {
                openDetails(openTorrent);
            } else if (getIntent() != null) {
                handleStartIntent();
            }
        } else {
            // Resume after instead of fully loading the torrents list; create connection and set action bar title
            ServerSetting lastUsed = applicationSettings.getLastUsedServer();
            currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
            serverSelectionView.updateCurrentServer(currentConnection);
            serverSelectionView.updateCurrentFilter(currentFilter);
        }
        firstStart = false;

        // Start the jobs for the background services, if needed
        ServerCheckerJob.schedule(getApplicationContext());
        RssCheckerJob.schedule(getApplicationContext());
        AppUpdateJob.schedule(getApplicationContext());

    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred
        if (drawerToggle != null) {
            drawerToggle.syncState();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        // update navigation labels
        navigationListAdapter.updateLabels(lastNavigationLabels);

        // Refresh server settings
        navigationListAdapter.updateServers(applicationSettings.getAllServerSettings());
        ServerSetting lastUsed = applicationSettings.getLastUsedServer();

        if (lastUsed == null) {
            // Still no settings
            updateFragmentVisibility(false);
            return;
        }

        // If we had no connection before, establish it now; otherwise just reload the settings
        if (currentConnection == null) {
            filterSelected(lastUsed, true);
        } else {
            currentConnection = lastUsed.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
        }

        // Start auto refresh
        startAutoRefresh();

    }

    @OnActivityResult(RESULT_DETAILS)
    protected void onDetailsScreenResult(Intent result) {
        // If the details activity returns whether the torrent was removed or updated, update the torrents list as well
        // (the details fragment is the source, so no need to update that)
        if (result != null && result.hasExtra("affected_torrent")) {
            Torrent affected = result.getParcelableExtra("affected_torrent");
            fragmentTorrents.quickUpdateTorrent(affected, result.getBooleanExtra("torrent_removed", false));
        }
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void startAutoRefresh() {
        // Check if already running
        if (autoRefreshTask != null || stopRefresh || systemSettings.getRefreshIntervalMilliseconds() == 0) {
            return;
        }

        autoRefreshTask = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                while (!isCancelled()) {
                    try {
                        Thread.sleep(systemSettings.getRefreshIntervalMilliseconds());
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                    // Just in case it was cancelled during sleep
                    if (isCancelled()) {
                        return null;
                    }

                    refreshTorrents();
                    if (Daemon.supportsStats(currentConnection.getType())) {
                        getAdditionalStats();
                    }
                }
                return null;
            }

        };
        autoRefreshTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    public void stopAutoRefresh() {
        if (autoRefreshTask != null) {
            autoRefreshTask.cancel(true);
        }
        autoRefreshTask = null;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        // Manually insert the actions into the main torrent and secondary actions toolbars
        torrentsToolbar.inflateMenu(R.menu.activity_torrents_main);
        if (actionsToolbar.getMenu().size() == 0) {
            actionsToolbar.inflateMenu(R.menu.activity_torrents_secondary);
        }
        if (navigationHelper.enableSearchUi()) {
            // Add an expandable SearchView to the action bar
            MenuItem item = menu.findItem(R.id.action_search);
            SearchView searchView = new SearchView(torrentsToolbar.getContext());
            searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
            searchView.setQueryRefinementEnabled(true);
            searchView.setOnSearchClickListener(v -> {
                // Pause autorefresh
                stopRefresh = true;
                stopAutoRefresh();
            });
            item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
                @Override
                public boolean onMenuItemActionExpand(MenuItem item) {
                    return true;
                }

                @Override
                public boolean onMenuItemActionCollapse(MenuItem item) {
                    stopRefresh = false;
                    startAutoRefresh();
                    return true;
                }
            });
            item.setActionView(searchView);
            searchMenu = item;
        }
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        // No connection yet; hide all menu options except settings
        if (currentConnection == null) {
            torrentsToolbar.setNavigationIcon(null);
            if (selectionToolbar != null)
                selectionToolbar.setVisibility(View.GONE);
            addmenuButton.setVisibility(View.GONE);
            actionsToolbar.setVisibility(View.GONE);
            if (filtersList != null)
                filtersList.setVisibility(View.GONE);
            filterSearch.setVisibility(View.GONE);
            torrentsToolbar.getMenu().findItem(R.id.action_search).setVisible(false);
            torrentsToolbar.getMenu().findItem(R.id.action_rss).setVisible(false);
            torrentsToolbar.getMenu().findItem(R.id.action_settings).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
            torrentsToolbar.getMenu().findItem(R.id.action_help).setVisible(true);
            actionsToolbar.getMenu().findItem(R.id.action_enableturtle).setVisible(false);
            actionsToolbar.getMenu().findItem(R.id.action_disableturtle).setVisible(false);
            actionsToolbar.getMenu().findItem(R.id.action_refresh).setVisible(false);
            actionsToolbar.getMenu().findItem(R.id.action_sort).setVisible(false);
            if (fragmentTorrents != null) {
                fragmentTorrents.updateConnectionStatus(false, null);
            }
            return true;
        }

        // There is a connection (read: settings to some server known)
        if (drawerToggle != null)
            torrentsToolbar.setNavigationIcon(R.drawable.ic_action_drawer);
        if (selectionToolbar != null)
            selectionToolbar.setVisibility(View.VISIBLE);
        addmenuButton.setVisibility(View.VISIBLE);
        actionsToolbar.setVisibility(View.VISIBLE);
        if (filtersList != null)
            filtersList.setVisibility(View.VISIBLE);
        filterSearch.setVisibility(View.VISIBLE);
        boolean addByFile = Daemon.supportsAddByFile(currentConnection.getType());
        addmenuFileButton.setVisibility(addByFile ? View.VISIBLE : View.GONE);
        // Primary toolbar menu
        torrentsToolbar.getMenu().findItem(R.id.action_search).setVisible(navigationHelper.enableSearchUi());
        torrentsToolbar.getMenu().findItem(R.id.action_rss).setVisible(navigationHelper.enableRssUi());
        torrentsToolbar.getMenu().findItem(R.id.action_settings).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
        torrentsToolbar.getMenu().findItem(R.id.action_help).setVisible(false);
        // Secondary toolbar menu
        boolean hasAltMode = Daemon.supportsSetAlternativeMode(currentConnection.getType());
        actionsToolbar.getMenu().findItem(R.id.action_enableturtle).setVisible(hasAltMode && !turtleModeEnabled);
        actionsToolbar.getMenu().findItem(R.id.action_disableturtle).setVisible(hasAltMode && turtleModeEnabled);
        actionsToolbar.getMenu().findItem(R.id.action_refresh).setVisible(true);
        actionsToolbar.getMenu().findItem(R.id.action_sort).setVisible(true);
        actionsToolbar.getMenu().findItem(R.id.action_sort_added).setVisible(Daemon.supportsDateAdded(currentConnection.getType()));
        if (fragmentTorrents != null) {
            fragmentTorrents.updateConnectionStatus(true, currentConnection.getType());
        }

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle only if this is the drawer toggle; otherwise the AndroidAnnotations will be used
        return drawerToggle != null && drawerToggle.onOptionsItemSelected(item);
    }

    /**
     * A new filter was selected; update the view over the current data
     *
     * @param item               The touched filter item
     * @param forceNewConnection Whether a new connection should be initialised regardless of the old server selection
     */
    protected void filterSelected(SimpleListItem item, boolean forceNewConnection) {

        // No longer apply the last used filter (on a fresh application start), if we still needed to
        preselectNavigationFilter = null;

        // Server selection
        if (item instanceof ServerSetting) {
            ServerSetting server = (ServerSetting) item;

            if (!forceNewConnection && currentConnection != null && server.equals(currentConnection.getSettings())) {
                // Already connected to this server; just ask for a refresh instead
                fragmentTorrents.updateIsLoading(true);
                refreshTorrents();
                return;
            }

            // Update connection to the newly selected server and refresh
            currentConnection = server.createServerAdapter(connectivityHelper.getConnectedNetworkName(), this);
            applicationSettings.setLastUsedServer(server);
            serverSelectionView.updateCurrentServer(currentConnection);
            if (forceNewConnection) {
                serverSelectionView.updateCurrentFilter(currentFilter);
            }

            // Clear the currently shown list of torrents and perhaps the details
            fragmentTorrents.clear(true, true);
            if (fragmentDetails != null && fragmentDetails.isResumed() && fragmentDetails.getActivity() != null) {
                fragmentDetails.updateIsLoading(false, null);
                fragmentDetails.clear();
                fragmentDetails.setCurrentServerSettings(server);
            }
            updateFragmentVisibility(true);
            refreshScreen();
            return;

        }

        // Status type or label selection - both of which are navigation filters
        if (item instanceof NavigationFilter) {
            // Set new filter
            currentFilter = (NavigationFilter) item;
            fragmentTorrents.applyNavigationFilter(currentFilter);
            serverSelectionView.updateCurrentFilter(currentFilter);
            // Remember that the user last selected this
            applicationSettings.setLastUsedNavigationFilter(currentFilter);
            // Clear the details view
            if (fragmentDetails != null && fragmentDetails.isResumed()) {
                fragmentDetails.updateIsLoading(false, null);
                fragmentDetails.clear();
            }
        }

    }

    /**
     * Hides the filter list and details fragment's full view if there is no configured connection
     *
     * @param hasServerSettings Whether there are server settings available, so we can continue to connect
     */
    private void updateFragmentVisibility(boolean hasServerSettings) {
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            if (hasServerSettings) {
                getSupportFragmentManager().beginTransaction().show(fragmentDetails).commit();
            } else {
                getSupportFragmentManager().beginTransaction().hide(fragmentDetails).commit();
            }
        }
        invalidateOptionsMenu();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        handleStartIntent();
    }

    protected void handleStartIntent() {
        // For intents that come from out of the application, perhaps we can not directly add them
        if (applicationSettings.getDefaultServerKey() == ApplicationSettings.DEFAULTSERVER_ASKONADD && getIntent().getData() != null) {
            // First ask which server to use before adding any intent from the extras
            ServerPickerDialog.startServerPicker(this, applicationSettings.getAllServerSettings());
            return;
        }
        addFromIntent();
    }

    public void switchServerAndAddFromIntent(int position) {
        // Callback from the ServerPickerDialog; force a connection before selecting it (in the navigation)
        // Note: we can just use the list position as we have stable server setting ids
        ServerSetting selectedServer = applicationSettings.getAllServerSettings().get(position);
        filterSelected(selectedServer, false);
        addFromIntent();
    }

    /**
     * If required, add torrents from the supplied intent extras.
     */
    protected void addFromIntent() {
        Intent intent = getIntent();
        Uri dataUri = intent.getData();
        String data = intent.getDataString();
        String action = intent.getAction();

        // Adding multiple torrents at the same time (as found in the Intent extras Bundle)
        if (action != null && action.equals("org.transdroid.ADD_MULTIPLE")) {
            // Intent should have some extras pointing to possibly multiple torrents
            String[] urls = intent.getStringArrayExtra("TORRENT_URLS");
            String[] titles = intent.getStringArrayExtra("TORRENT_TITLES");
            if (urls != null) {
                for (int i = 0; i < urls.length; i++) {
                    String title = (titles != null && titles.length >= i ? titles[i] : NavigationHelper.extractNameFromUri(Uri.parse(urls[i])));
                    if (intent.hasExtra("PRIVATE_SOURCE")) {
                        // This is marked by the Search Module as being a private source site; get the url locally first
                        addTorrentFromPrivateSource(urls[i], title, intent.getStringExtra("PRIVATE_SOURCE"));
                    } else {
                        addTorrentByUrl(urls[i], title);
                    }
                }
            }
            return;
        }

        // Add a torrent from a local or remote data URI?
        if (dataUri == null) {
            return;
        }
        if (dataUri.getScheme() == null) {
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_invalid_url_form).colorResource(R.color.red));
            return;
        }

        // Get torrent title
        String title = NavigationHelper.extractNameFromUri(dataUri);
        if (intent.hasExtra("TORRENT_TITLE")) {
            title = intent.getStringExtra("TORRENT_TITLE");
        }

        // Adding a torrent from the Android downloads manager
        if (dataUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
            addTorrentFromDownloads(dataUri, title);
            return;
        }

        // Adding a torrent from http or https URL
        if (dataUri.getScheme().equals("http") || dataUri.getScheme().equals("https")) {

            String privateSource = getIntent().getStringExtra("PRIVATE_SOURCE");

            WebsearchSetting match = null;
            if (privateSource == null) {
                // Check if the target URL is also defined as a web search in the user's settings
                List<WebsearchSetting> websearches = applicationSettings.getWebsearchSettings();
                for (WebsearchSetting setting : websearches) {
                    Uri uri = Uri.parse(setting.getBaseUrl());
                    if (uri.getHost() != null && uri.getHost().equals(dataUri.getHost())) {
                        match = setting;
                        break;
                    }
                }
            }

            // If the URL is also a web search and it defines cookies, use the cookies by downloading the targeted
            // torrent file (while supplies the cookies to the HTTP request) instead of sending the URL directly to the
            // torrent client. If instead it is marked (by the Torrent Search module) as being form a private site, use
            // the Search Module instead to download the url locally first.
            if (match != null && match.getCookies() != null) {
                addTorrentFromWeb(data, match, title);
            } else if (privateSource != null) {
                addTorrentFromPrivateSource(data, title, privateSource);
            } else {
                // Normally send the URL to the torrent client
                addTorrentByUrl(data, title);
            }
            return;
        }

        // Adding a torrent from magnet URL
        if (dataUri.getScheme().equals("magnet")) {
            addTorrentByMagnetUrl(data, title);
            return;
        }

        // Adding a local .torrent file; the title we show is just the file name
        if (dataUri.getScheme().equals("file")) {
            addTorrentByFile(data, title);
        }

    }

    @Override
    protected void onPause() {
        if (searchMenu != null) {
            searchMenu.collapseActionView();
        }
        stopAutoRefresh();
        super.onPause();
    }

    @Override
    public boolean onSearchRequested() {
        if (searchMenu != null) {
            searchMenu.expandActionView();
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (awaitingAddLocalFile != null && awaitingAddTitle != null &&
                Boolean.TRUE.equals(navigationHelper.handleTorrentReadPermissionResult(requestCode, grantResults))) {
            addTorrentByFile(awaitingAddLocalFile, awaitingAddTitle);
        }
    }

    @Click(R.id.addmenu_link_button)
    protected void startUrlEntryDialog() {
        addmenuButton.collapse();
        UrlEntryDialog.show(this);
    }

    @Click(R.id.addmenu_file_button)
    protected void startFilePicker() {
        addmenuButton.collapse();
        FilePickerHelper.startFilePicker(this);
    }

    @Background
    @OnActivityResult(FilePickerHelper.ACTIVITY_FILEPICKER)
    public void onFilePicked(int resultCode, Intent data) {
        // We should have received an Intent with a local torrent's Uri as data from the file picker
        if (data != null && data.getData() != null && !data.getData().toString().equals("")) {
            Uri dataUri = data.getData();

            // Get torrent title
            String title = NavigationHelper.extractNameFromUri(dataUri);

            // Adding a torrent from the via a content:// scheme (access through content provider stream)
            if (dataUri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
                addTorrentFromDownloads(dataUri, title);
                return;
            }

            // Adding a .torrent file directly via the file:// scheme (we can access it directly)
            if (dataUri.getScheme().equals("file")) {
                addTorrentByFile(data.getDataString(), title);
            }

        }
    }

    @OptionsItem(R.id.action_refresh)
    public void refreshScreen() {
        if (fragmentTorrents.isAdded())
            fragmentTorrents.updateIsLoading(true);
        refreshTorrents();
        if (Daemon.supportsStats(currentConnection.getType())) {
            getAdditionalStats();
        }
    }

    @OptionsItem(R.id.action_enableturtle)
    protected void enableTurtleMode() {
        updateTurtleMode(true);
    }

    @OptionsItem(R.id.action_disableturtle)
    protected void disableTurtleMode() {
        updateTurtleMode(false);
    }

    @OptionsItem(R.id.action_rss)
    protected void openRss() {
        RssFeedsActivity_.intent(this).start();
    }

    @OptionsItem(R.id.action_settings)
    protected void openSettings() {
        MainSettingsActivity_.intent(this).start();
    }

    @OptionsItem(R.id.action_help)
    protected void openHelp() {
        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/download/")));
    }

    @OptionsItem(R.id.action_sort_byname)
    protected void sortByName() {
        fragmentTorrents.sortBy(TorrentsSortBy.Alphanumeric);
    }

    @OptionsItem(R.id.action_sort_status)
    protected void sortByStatus() {
        fragmentTorrents.sortBy(TorrentsSortBy.Status);
    }

    @OptionsItem(R.id.action_sort_done)
    protected void sortByDateDone() {
        fragmentTorrents.sortBy(TorrentsSortBy.DateDone);
    }

    @OptionsItem(R.id.action_sort_added)
    protected void sortByDateAdded() {
        fragmentTorrents.sortBy(TorrentsSortBy.DateAdded);
    }

    @OptionsItem(R.id.action_sort_percent)
    protected void sortByPercent() {
        fragmentTorrents.sortBy(TorrentsSortBy.Percent);
    }

    @OptionsItem(R.id.action_sort_downspeed)
    protected void sortByDownspeed() {
        fragmentTorrents.sortBy(TorrentsSortBy.DownloadSpeed);
    }

    @OptionsItem(R.id.action_sort_upspeed)
    protected void sortByUpspeed() {
        fragmentTorrents.sortBy(TorrentsSortBy.UploadSpeed);
    }

    @OptionsItem(R.id.action_sort_ratio)
    protected void sortByRatio() {
        fragmentTorrents.sortBy(TorrentsSortBy.Ratio);
    }

    @OptionsItem(R.id.action_sort_size)
    protected void sortBySize() {
        fragmentTorrents.sortBy(TorrentsSortBy.Size);
    }

    /**
     * Redirect the newly entered list filter to the torrents fragment.
     *
     * @param newFilterText The newly entered filter (or empty to clear the current filter).
     */
    public void filterTorrents(String newFilterText) {
        fragmentTorrents.applyTextFilter(newFilterText);
    }

    /**
     * Shows the a details fragment for the given torrent, either in the dedicated details fragment pane, in the same pane as the torrent list was
     * displayed or by starting a details activity.
     *
     * @param torrent The torrent to show detailed statistics for
     */
    public void openDetails(Torrent torrent) {
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            fragmentDetails.updateTorrent(torrent);
        } else {
            DetailsActivity_.intent(this).torrent(torrent).currentLabels(lastNavigationLabels).startForResult(RESULT_DETAILS);
        }
    }

    @Background
    protected void refreshTorrents() {
        String startConnectionId = currentConnection.getSettings().getIdString();
        DaemonTaskResult result = RetrieveTask.create(currentConnection).execute(log);
        if (!startConnectionId.equals(currentConnection.getSettings().getIdString())) {
            // During the command execution the user changed the server, so we are no longer interested in the result
            return;
        }
        if (result instanceof RetrieveTaskSuccessResult) {
            onTorrentsRetrieved(((RetrieveTaskSuccessResult) result).getTorrents(), ((RetrieveTaskSuccessResult) result).getLabels());
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, true);
        }
    }

    @Background
    public void refreshTorrentDetails(Torrent torrent) {
        if (!Daemon.supportsFineDetails(currentConnection.getType())) {
            return;
        }
        String startConnectionId = currentConnection.getSettings().getIdString();
        DaemonTaskResult result = GetTorrentDetailsTask.create(currentConnection, torrent).execute(log);
        if (!startConnectionId.equals(currentConnection.getSettings().getIdString())) {
            // During the command execution the user changed the server, so we are no longer interested in the result
            return;
        }
        if (result instanceof GetTorrentDetailsTaskSuccessResult) {
            onTorrentDetailsRetrieved(torrent, ((GetTorrentDetailsTaskSuccessResult) result).getTorrentDetails());
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    public void refreshTorrentFiles(Torrent torrent) {
        if (!Daemon.supportsFileListing(currentConnection.getType())) {
            return;
        }
        String startConnectionId = currentConnection.getSettings().getIdString();
        DaemonTaskResult result = GetFileListTask.create(currentConnection, torrent).execute(log);
        if (!startConnectionId.equals(currentConnection.getSettings().getIdString())) {
            // During the command execution the user changed the server, so we are no longer interested in the result
            return;
        }
        if (result instanceof GetFileListTaskSuccessResult) {
            onTorrentFilesRetrieved(torrent, ((GetFileListTaskSuccessResult) result).getFiles());
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    protected void getAdditionalStats() {
        String startConnectionId = currentConnection.getSettings().getIdString();
        DaemonTaskResult result = GetStatsTask.create(currentConnection).execute(log);
        if (!startConnectionId.equals(currentConnection.getSettings().getIdString())) {
            // During the command execution the user changed the server, so we are no longer interested in the result
            return;
        }
        if (result instanceof GetStatsTaskSuccessResult) {
            onTurtleModeRetrieved(((GetStatsTaskSuccessResult) result).isAlternativeModeEnabled());
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    protected void updateTurtleMode(boolean enable) {
        String startConnectionId = currentConnection.getSettings().getIdString();
        DaemonTaskResult result = SetAlternativeModeTask.create(currentConnection, enable).execute(log);
        if (!startConnectionId.equals(currentConnection.getSettings().getIdString())) {
            // During the command execution the user changed the server, so we are no longer interested in the result
            return;
        }
        if (result instanceof DaemonTaskSuccessResult) {
            // Success; no need to retrieve it again - just update the visual indicator
            onTurtleModeRetrieved(enable);
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    public void addTorrentByUrl(String url, String title) {
        DaemonTaskResult result = AddByUrlTask.create(currentConnection, url, title).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
            refreshTorrents();
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    public void addTorrentByMagnetUrl(String url, String title) {

        // Since v39 Chrome sends application/x-www-form-urlencoded magnet links and most torrent clients do not understand those, so decode first
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Ignore: UTF-8 is always available on Android devices
        } catch (IllegalArgumentException e) {
            // Illegal character or escape sequence; fail task to show error
            onCommunicationError(new DaemonTaskFailureResult(AddByMagnetUrlTask.create(currentConnection, url),
                    new DaemonException(DaemonException.ExceptionType.FileAccessError, "Invalid characters in magnet link")), false);
            return;
        }

        AddByMagnetUrlTask addByMagnetUrlTask = AddByMagnetUrlTask.create(currentConnection, url);
        if (!Daemon.supportsAddByMagnetUrl(currentConnection.getType())) {
            // No support for magnet links: forcefully let the task fail to report the error
            onCommunicationError(new DaemonTaskFailureResult(addByMagnetUrlTask, new DaemonException(DaemonException.ExceptionType.MethodUnsupported,
                    currentConnection.getType().name() + " does not support magnet links")), false);
            return;
        }

        DaemonTaskResult result = addByMagnetUrlTask.execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
            refreshTorrents();
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }

    }

    @Background
    protected void addTorrentByFile(String localFile, String title) {
        if (!navigationHelper.checkTorrentReadPermission(this)) {
            // No read permission yet (which we get the result of in onRequestPermissionsResult)
            awaitingAddLocalFile = localFile;
            awaitingAddTitle = title;
            return;
        }
        DaemonTaskResult result = AddByFileTask.create(currentConnection, localFile).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_added, title));
            refreshTorrents();
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    private void addTorrentFromDownloads(Uri contentUri, String title) {

        try {
            // Open the content uri as input stream and this via a local temporary file
            addTorrentFromStream(getContentResolver().openInputStream(contentUri), title);
        } catch (SecurityException e) {
            // No longer access to this file
            log.e(this, "No access given to " + contentUri.toString() + ": " + e.toString());
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
        } catch (FileNotFoundException e) {
            log.e(this, contentUri.toString() + " does not exist: " + e.toString());
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
        }
    }

    @Background
    protected void addTorrentFromPrivateSource(String url, String title, String source) {

        try {
            InputStream input = SearchHelper_.getInstance_(this).getFile(source, url);
            addTorrentFromStream(input, title);
        } catch (Exception e) {
            log.e(this, "Can't download private site torrent " + url + " from " + source + ": " + e.toString());
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
        }

    }

    @Background
    protected void addTorrentFromWeb(String url, WebsearchSetting websearchSetting, String title) {

        try {
            // Cookies are taken from the websearchSetting that we already matched against this target URL
            DefaultHttpClient httpclient = HttpHelper.createStandardHttpClient(false, null, null, true, null, 10000, null, -1);
            Map<String, String> cookies = HttpHelper.parseCookiePairs(websearchSetting.getCookies());
            String domain = Uri.parse(url).getHost();
            for (Entry<String, String> pair : cookies.entrySet()) {
                BasicClientCookie cookie = new BasicClientCookie(pair.getKey(), pair.getValue());
                cookie.setPath("/");
                cookie.setDomain(domain);
                httpclient.getCookieStore().addCookie(cookie);
            }

            // Download the torrent at the specified URL (which will first be written to a temporary file)
            // If we get an HTTP 401, 403 or 404 response, show an error to the user
            HttpResponse response = httpclient.execute(new HttpGet(url));
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED ||
                    response.getStatusLine().getStatusCode() == HttpStatus.SC_FORBIDDEN ||
                    response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND) {
                log.e(this, "Can't retrieve web torrent " + url + ": Unexpected HTTP response status code " +
                        response.getStatusLine().toString());
                SnackbarManager.show(Snackbar.with(this).text(R.string.error_401).colorResource(R.color.red));
                return;
            }
            InputStream input = response.getEntity().getContent();
            addTorrentFromStream(input, title);
        } catch (Exception e) {
            log.e(this, "Can't retrieve web torrent " + url + ": " + e.toString());
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
        }
    }

    @Background
    protected void addTorrentFromStream(InputStream input, String title) {

        File tempFile = new File("/not/yet/set");
        try {
            // Write a temporary file with the torrent contents
            tempFile = File.createTempFile("transdroid_", ".torrent", getCacheDir());
            try (FileOutputStream output = new FileOutputStream(tempFile)) {
                final byte[] buffer = new byte[1024];
                int read;
                while ((read = input.read(buffer)) != -1) {
                    output.write(buffer, 0, read);
                }
                output.flush();
                String fileName = Uri.fromFile(tempFile).toString();
                addTorrentByFile(fileName, title);
            }
        } catch (IOException e) {
            log.e(this, "Can't write input stream to " + tempFile.toString() + ": " + e.toString());
            SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException e) {
                log.e(this, "Error closing the input stream " + tempFile.toString() + ": " + e.toString());
                SnackbarManager.show(Snackbar.with(this).text(R.string.error_torrentfile).colorResource(R.color.red));
            }
        }
    }

    @Background
    @Override
    public void resumeTorrent(Torrent torrent) {
        torrent.mimicResume();
        DaemonTaskResult result = ResumeTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_resumed, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void pauseTorrent(Torrent torrent) {
        torrent.mimicPause();
        DaemonTaskResult result = PauseTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_paused, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void startTorrent(Torrent torrent, boolean forced) {
        torrent.mimicStart();
        DaemonTaskResult result = StartTask.create(currentConnection, torrent, forced).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_started, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void stopTorrent(Torrent torrent) {
        torrent.mimicStop();
        DaemonTaskResult result = StopTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_stopped, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void removeTorrent(Torrent torrent, boolean withData) {
        DaemonTaskResult result = RemoveTask.create(currentConnection, torrent, withData).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result,
                    getString(withData ? R.string.result_removed_with_data : R.string.result_removed, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void updateLabel(Torrent torrent, String newLabel) {
        torrent.mimicNewLabel(newLabel);
        DaemonTaskResult result = SetLabelTask.create(currentConnection, torrent, newLabel == null ? "" : newLabel).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result,
                    newLabel == null ? getString(R.string.result_labelremoved) : getString(R.string.result_labelset, newLabel));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) {
        torrent.mimicSequentialDownload(sequentialState);
        DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) {
        torrent.mimicFirstLastPieceDownload(firstLastPieceState);
        DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.action_toggle_firstlastpiece));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void forceRecheckTorrent(Torrent torrent) {
        torrent.mimicCheckingStatus();
        DaemonTaskResult result = ForceRecheckTask.create(currentConnection, torrent).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_recheckedstarted, torrent.getName()));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void updateTrackers(Torrent torrent, List<String> newTrackers) {
        DaemonTaskResult result = SetTrackersTask.create(currentConnection, torrent, newTrackers).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_trackersupdated));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void updateLocation(Torrent torrent, String newLocation) {
        DaemonTaskResult result = SetDownloadLocationTask.create(currentConnection, torrent, newLocation).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_locationset, newLocation));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    @Override
    public void updatePriority(Torrent torrent, List<TorrentFile> files, Priority priority) {
        DaemonTaskResult result = SetFilePriorityTask.create(currentConnection, torrent, priority, new ArrayList<>(files)).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_priotitiesset));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @Background
    public void updateMaxSpeeds(Integer maxDownloadSpeed, Integer maxUploadSpeed) {
        DaemonTaskResult result = SetTransferRatesTask.create(currentConnection, maxUploadSpeed, maxDownloadSpeed).execute(log);
        if (result instanceof DaemonTaskSuccessResult) {
            onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_maxspeedsset));
        } else {
            onCommunicationError((DaemonTaskFailureResult) result, false);
        }
    }

    @UiThread
    protected void onTaskSucceeded(DaemonTaskSuccessResult result, String successMessage) {
        // Refresh the screen as well
        refreshScreen();
        SnackbarManager.show(Snackbar.with(this).text(successMessage));
    }

    @UiThread
    protected void onCommunicationError(DaemonTaskFailureResult result, boolean isCritical) {
        log.i(this, result.getException().toString());
        String error = getString(LocalTorrent.getResourceForDaemonException(result.getException()));
        SnackbarManager.show(Snackbar.with(this).text(error).colorResource(R.color.red).type(SnackbarType.MULTI_LINE));
        fragmentTorrents.updateIsLoading(false);
        if (isCritical) {
            fragmentTorrents.updateError(error);
            if (fragmentDetails != null && fragmentDetails.isResumed()) {
                fragmentDetails.updateIsLoading(false, error);
            }
        }
    }

    @UiThread
    protected void onTorrentsRetrieved(List<Torrent> torrents, List<org.transdroid.daemon.Label> labels) {

        lastNavigationLabels = Label.convertToNavigationLabels(labels, getResources().getString(R.string.labels_unlabeled));

        // Report the newly retrieved list of torrents to the torrents fragment
        fragmentTorrents.updateIsLoading(false);
        fragmentTorrents.updateTorrents(new ArrayList<>(torrents), lastNavigationLabels);

        // Update the details fragment if the currently shown torrent is in the newly retrieved list
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            fragmentDetails.perhapsUpdateTorrent(torrents);
        }

        // Update local list of labels in the navigation
        navigationListAdapter.updateLabels(lastNavigationLabels);
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            fragmentDetails.updateLabels(lastNavigationLabels);
        }

        // Perhaps we were still waiting to preselect the last used filter (on a fresh application start)
        if (preselectNavigationFilter != null && navigationListAdapter != null) {
            for (int i = 0; i < navigationListAdapter.getCount(); i++) {
                // Look up the navigation filter item, which is represented as simple list item (and might not exist any
                // more, such as with a label that is deleted on the server)
                Object item = navigationListAdapter.getItem(i);
                if (item instanceof SimpleListItem && item instanceof NavigationFilter &&
                        ((NavigationFilter) item).getCode().equals(preselectNavigationFilter)) {
                    filterSelected((SimpleListItem) item, false);
                    break;
                }
            }
            // Only preselect after the first update we receive (even if the filter wasn't found any more)
            preselectNavigationFilter = null;
        }

        // Update the server status (counts and speeds) in the action bar
        serverStatusView
                .updateStatus(torrents, systemSettings.treatDormantAsInactive(), Daemon.supportsSetTransferRates(currentConnection.getType()));

    }

    @UiThread
    protected void onTorrentDetailsRetrieved(Torrent torrent, TorrentDetails torrentDetails) {
        // Update the details fragment with the new fine details for the shown torrent
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            fragmentDetails.updateTorrentDetails(torrent, torrentDetails);
        }
    }

    @UiThread
    protected void onTorrentFilesRetrieved(Torrent torrent, List<TorrentFile> torrentFiles) {
        // Update the details fragment with the newly retrieved list of files
        if (fragmentDetails != null && fragmentDetails.isResumed()) {
            fragmentDetails.updateTorrentFiles(torrent, new ArrayList<>(torrentFiles));
        }
    }

    @UiThread
    protected void onTurtleModeRetrieved(boolean turtleModeEnabled) {
        this.turtleModeEnabled = turtleModeEnabled;
        invalidateOptionsMenu();
    }

}
